/*
 * Copyright 2014 Matyas Mazzag
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.mcubed.soapui


import java.security.SecureRandom

import com.eviware.soapui.impl.wsdl.teststeps.WsdlTestRequestStep
import com.eviware.soapui.support.XmlHolder
import com.eviware.soapui.support.xml.XmlUtils


/**
 * TODO: Support for databases?  Leave that to whatever baseclass the user decides to have??
 * 
 * @author Matyas Mazzag
 *
 */
abstract class BaseScript extends Script {
	static def final WORD_UPPER = { n -> n.toUpperCase() } 
	static def final WORD_LOWER = { n -> n.toLowerCase() } 
	static def final WORD_SENTENCE = { n -> n.substring( 0, 1 ).toUpperCase() + n.substring( 1 ).toLowerCase() } 
	protected final XmlUtils xmlUtils
	protected final SecureRandom random

	
	/*
	 * Default constructor.
	 */
	BaseScript() {
		this.xmlUtils = new XmlUtils() 
		this.random = new SecureRandom()
		// Enhance XmlHolder class with EGL.i's helper methods.
		XmlHolder.metaClass.newNodes = { r, n -> createElements( delegate, r, n ) }
		XmlHolder.metaClass.nofNodes = { np, x -> ensureXNumberOfElements( delegate, np, x ) }
		XmlHolder.metaClass.update = { -> delegate.updateProperty( true ) }
	}

	/*
	 * This method should be used by overriding classes (scripts) to further 
	 * prepare any data they might be using.
	 * A typical use would be adding fields or closures to the context which
	 * is null in the constructor.
	 */
	def init() {}
		
	/*
	 * Helper method to set a property.
	 */
	private def setTestCaseProperty( String propertyName, propertyValue ) {
		testRunner?.testCase?.setPropertyValue( propertyName, propertyValue )
	}	

	/*
	 * Helper method to get a property.
	 */
	def getTestCaseProperty( String propertyName ) {
		return testRunner?.testCase?.properties[ propertyName ]?.value
	}

	/*
	 * Returns all Wsdl test steps.
	 */
	def getWsdlTestSteps() {
		return testRunner?.testCase?.testStepList.findAll {
					it.class == com.eviware.soapui.impl.wsdl.teststeps.WsdlTestRequestStep.class
		}
	}
	
	/*
	 * Helper to figure out the next request step.
	 */
	def getNextWsdlTestStep() {
		return testRunner?.testCase?.testStepList[ Math.min(context.currentStepIndex,context.currentStepIndex+1)..(testRunner.testCase.testStepList.size()-1) ].find {
			it.class == WsdlTestRequestStep.class
		}
	}

	/*
	 * Helper to figure out the previous request step.
	 */
	def getPreviousWsdlTestStep() {
		return testRunner?.testCase?.testStepList[ 0..Math.max(0,context.currentStepIndex-1) ].reverse().find {
			it.class == WsdlTestRequestStep.class
		}
	}

	/*
	 * Helper to load the specified configuration from properties files as
	 * referenced by the TestCase property name.
	 */
	def loadConfig( String configPropertyName = '.main.props' ) {
		def fileName = getTestCaseProperty( configPropertyName )
		def configFile = configResolver.resolve( fileName )
		def lastModified = getTestCaseProperty( "${configPropertyName}.modified" ) as long
		if( !lastModified || configFile.lastModified() > lastModified ) {
			def properties = loadConfigProperties( configFile )
			clearTestCaseProperties()
			setTestCaseProperties( properties )
			setTestCaseProperty( "${configPropertyName}.modified", configFile.lastModified() as String )
		}
	}

	/*
	 * Helper to recursively read all properties from a hierarchy of properties files.
	 */
	private def loadConfigProperties( File configFile ) {
		def extProps = [:]
		def allProps = [:]
		configFile.withInputStream {
			new Properties() {{
				load( new BufferedInputStream( it ))
				it.close()
			}}.each { key, value ->
				if( key.endsWith( '.props' )) {
					extProps.put( key, "${configFile.parent}/$value" )
				} else {
					allProps.put( key, value )
				}
			}
		}
		extProps.each { key, value ->
			allProps << loadConfigProperties( new File( value ))
		}
		return allProps
	}
		
	/*
	 * Helper to copy properties to the TestCase
	 */
	private def setTestCaseProperties( String environment = null, Map properties ) {
		properties.each { key, value ->
			if ( environment ) {
				if( key.startsWith( "$environment." )) {
					setTestCaseProperty( key.replaceFirst( /$environment\./, '' ), value )
				}
			} else {
				setTestCaseProperty( key, value )
			}
		}
	}

	/*
	 * Helper to clear properties from a TestCase and ignore others.
	 */
	def clearTestCaseProperties( List ignoredProperties = [] ) {
		testRunner?.testCase?.properties.each { key, value ->
			if( !key.startsWith( '.' ) && !ignoredProperties.contains( key )) {
				testRunner.testCase.removeProperty( key )
			}
		}
	}

	/*
	 * Sets all WsdlTestRequestStep endpoints to the configured one from properties
	 */
	def setEndPoints( String hostProperty, String portProperty ) {
		def wsHost = getTestCaseProperty( hostProperty )
		def wsPort = getTestCaseProperty( portProperty )
		setEndPoints( "${wsHost}:${wsPort}" )
	}

	/*
	 * Sets all WsdlTestRequestStep endpoints to the passed host:port.
	 */
	def setEndPoints( String hostAndPort ) {
		testRunner?.testCase?.testStepList?.findAll{ it.class == WsdlTestRequestStep.class }.each {
		  it.properties[ 'Endpoint' ].value = it.properties[ 'Endpoint' ].value.replaceAll( /http:\\/\\/[^\/]+/, "http://$hostAndPort" )
		}
	}
	
	/*
	 * Helper to get the user logged on to this computer 
	 */
	def getUserName() {
		return System.properties[ 'user.name' ]
	}
	
	/*
	 * Helper to get the name of this pc 
	 */
	def getHostName() {
		return Inet4Address.localHost.hostName
	}

	/*
	 * Helper to create elements under the specified root element.
	 */
	protected def createElements( XmlHolder holder, String rootElementPath, nodeNames = [] ) {
		def rootElement = holder.getDomNode( rootElementPath )
		nodeNames.collect{
			ensureXNumberOfElements( holder, "$rootElementPath/$it", 0 )
			xmlUtils.createNodeFromPath( rootElement, "/$it" )
		}
	}

	/*
	 * Guarantees that x number of nodes of type nodePath exist
	 * It is required that at least one node exists at nodePath.
	 * To create one see {@link #createElements()} 
	 */
	protected def ensureXNumberOfElements( XmlHolder holder, nodePath, x ) {
		// Get prototype:
		def prototypeNode = holder.getDomNode( nodePath + '[1]' )
		if( prototypeNode ) {
			// Get parent:
			def prototypeParentNode = prototypeNode.parentNode

			// Remove all children:
			removeAllChildren( prototypeParentNode )

			// Create new nodes by cloning prototype:
			addAsChildXTimes( prototypeParentNode, prototypeNode, x )
		}
	}

	/*
	 * Removes all children of parentNode.
	 */
	private def removeAllChildren( parentNode ) {
		while( parentNode.firstChild ) {
			parentNode.removeChild( parentNode.firstChild )
		}
	}

	/*
	 * Adds the childNode X times to parentNode.
	 */
	private def addAsChildXTimes( parentNode, childNode, x ) {
 		x.times {
			def newChildNode = childNode.cloneNode( true )
			if( parentNode.ownerDocument != childNode.ownerDocument ) {
				newChildNode = parentNode.ownerDocument.importNode( newChildNode, true )
			}
			parentNode.appendChild( newChildNode )
		}
	}
			
	/*
	 * Helper to set a context field to a value
	 */
	def setContextField( String contextFieldName, value ) {
		context."$contextFieldName" = value
	}

	/*
	 * Dynamically creates a method to lookup the context field or
	 * return the default value.
	 * 
	 * The result of calling this method is that the context will have a
	 * closure created on it for the property name (for property name
	 * "abc.def" the closure will be called abcDef(), so after calling
	 * this method context.abcDef() will exist).
	 * 
	 * The closure will either return the property's value if it exists
	 * or the default value as produced by the passed closure.
	 * 
	 */
	def createLookupForProperty( String propertyName, Closure defaultClosure ) {
		def contextFieldName = propertyNameToContextFieldName( propertyName )
		context."$contextFieldName" = { 
			def contextFieldValueName = "${contextFieldName}Value"
			if( context."$contextFieldValueName" ) { 
				return context."$contextFieldValueName"
			} else {
				if( getTestCaseProperty( propertyName )) {
					return context."$contextFieldValueName" = getTestCaseProperty( propertyName )
				} else {
					return context."$contextFieldValueName" = defaultClosure?.call()
				}
			}
		}
	}
	
	/*
	 * Creates a context field name from a property name.
	 * 
	 * Example: from "abc.def" "abcDef" will be returned.
	 *  
	 */
	private def propertyNameToContextFieldName( propertyName ) {
		def contextFieldName = ''
		propertyName.split( /\./ ).collect{ contextFieldName += it.substring( 0, 1 ).toUpperCase() + it.substring( 1 )}
		return contextFieldName.substring( 0, 1 ).toLowerCase() + contextFieldName.substring( 1 )
	}

	/*
	 * Returns all Soap requests within the test case.
	 */
	def getSoapRequests() {
		return getWsdlTestSteps().collect{
			groovyUtils.getXmlHolder( it.name + '#Request' )
		}
	}

	/*
	 * Returns all Soap responses within the test case.
	 */
	def getSoapResponses() {
		return getWsdlTestSteps().collect{
			groovyUtils.getXmlHolder( it.name + '#Response' )
		}
	}
	
	/*
	 * Helper to get a handle to the next SoapRequest object
	 */
	def getNextSoapRequest() {
		return groovyUtils.getXmlHolder( getNextWsdlTestStep()?.name + '#Request' )
	}

	/*
	 * Helper to get a handle to the previous SoapRequest object
	 */
	def getPreviousSoapRequest() {
		return groovyUtils.getXmlHolder( getPreviousWsdlTestStep()?.name + '#Request' )
	}

	/*
	 * Helper to get a handle to the previous SoapResponse object
	 */
	def getPreviousSoapResponse() {
		return groovyUtils.getXmlHolder( getPreviousWsdlTestStep()?.name + '#Response' )
	}

	/*
	* Generates a completely random name out of characters.
	* The resulting name is in upper case and contains only
	* alphanumerics.
	*/
	def generateWord( int minLength = 1, int maxLength = Math.max(minLength, 15), Closure transform = null ) {
		assert minLength > 0 : 'use minLength > 0 when generating words'
		def name = ''
		def length = generateNumberInRange( minLength, Math.max(minLength, maxLength ))
		length.times { 
			def character = generateCharacter()
			if( length == 2 && it == 1 ) {
				if( isVowel( ''+name.charAt(0) ) ) {
					while( isVowel( character )) {
						character = generateCharacter()
					}
				} else {
					while( !isVowel( character )) {
						character = generateCharacter()
					}
				}
			} else {
				if( it > 1 ) {
					if( isVowel( ''+name.charAt(name.length()-2) ) && isVowel( ''+name.charAt(name.length()-1) )) {
						while( isVowel( character )) {
							character = generateCharacter()
						}
					} else if( !isVowel( ''+name.charAt(name.length()-2) ) && !isVowel( ''+name.charAt(name.length()-1) )) {
						while( !isVowel( character )) {
							character = generateCharacter()
						}
					}
				}
			}
			name += character
		}
		
		return transform ? transform( name ) : name;
	}
	
	/*
	* Checks if the character is a vowel.
	*/
	def isVowel( CharSequence character ) {
		return 'AEIOU'.indexOf( character ) < 0
	}

	/*
	* Generates an upper case character.
	*/
	def generateCharacter() {
		return (generateNumberInRange( (('A' as char) as int), (('Z' as char) as int )) as char) as String
	}
	
	/*
	* Generates a number in the specified range (inclusive of lower and upper)
	* and zeros pads the result to have the same length as the max value in the range.
	* For example generatePaddedNumberInRange(1, 10) will generate results like 06, 09, 10, etc.
	*/
	def generatePaddedNumberInRange( int lower, int upper ) {
		assert lower <= upper : 'use lower <= upper when generating padded number in range'
		return String.format( '%0' + (''+upper).length() + 'd', generateNumberInRange( lower, upper ))
	}
	
	/*
	* Generates a number in the specified range (inclusive of lower and upper).
	*/
	def generateNumberInRange( int lower, int upper ) {
		assert lower <= upper : 'use lower <= upper when generating number in range'
		return random.nextInt( upper - lower + 1 ) + lower
	}
	
	/*
	* Generates zeros
	*/
	def generateZeros( int length ) {
		assert length > 0 : 'use length > 0 when generating zeros'
		return String.format( "%0${length}d", 0 )
	}

}